*/ public array $forbiddenClasses = []; /** @var array */ public array $forbiddenExtends = []; /** @var array */ public array $forbiddenInterfaces = []; /** @var array */ public array $forbiddenTraits = []; /** @var list */ private static array $keywordReferences = ['self', 'parent', 'static']; /** * @return array */ public function register(): array { $searchTokens = []; if (count($this->forbiddenClasses) > 0) { $this->forbiddenClasses = self::normalizeInputOption($this->forbiddenClasses); $searchTokens[] = T_NEW; $searchTokens[] = T_DOUBLE_COLON; } if (count($this->forbiddenExtends) > 0) { $this->forbiddenExtends = self::normalizeInputOption($this->forbiddenExtends); $searchTokens[] = T_EXTENDS; } if (count($this->forbiddenInterfaces) > 0) { $this->forbiddenInterfaces = self::normalizeInputOption($this->forbiddenInterfaces); $searchTokens[] = T_IMPLEMENTS; } if (count($this->forbiddenTraits) > 0) { $this->forbiddenTraits = self::normalizeInputOption($this->forbiddenTraits); $searchTokens[] = T_USE; } return $searchTokens; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $tokenPointer */ public function process(File $phpcsFile, $tokenPointer): void { $tokens = $phpcsFile->getTokens(); $token = $tokens[$tokenPointer]; $nameTokens = [...TokenHelper::NAME_TOKEN_CODES, ...TokenHelper::INEFFECTIVE_TOKEN_CODES]; if ( $token['code'] === T_IMPLEMENTS || ( $token['code'] === T_USE && UseStatementHelper::isTraitUse($phpcsFile, $tokenPointer) ) ) { $endTokenPointer = TokenHelper::findNext( $phpcsFile, [T_SEMICOLON, T_OPEN_CURLY_BRACKET], $tokenPointer, ); $references = $this->getAllReferences($phpcsFile, $tokenPointer, $endTokenPointer); if ($token['code'] === T_IMPLEMENTS) { $this->checkReferences($phpcsFile, $tokenPointer, $references, $this->forbiddenInterfaces); } else { // Fixer does not work when traits contains aliases $this->checkReferences( $phpcsFile, $tokenPointer, $references, $this->forbiddenTraits, $tokens[$endTokenPointer]['code'] !== T_OPEN_CURLY_BRACKET, ); } } elseif (in_array($token['code'], [T_NEW, T_EXTENDS], true)) { $endTokenPointer = TokenHelper::findNextExcluding($phpcsFile, $nameTokens, $tokenPointer + 1); $references = $this->getAllReferences($phpcsFile, $tokenPointer, $endTokenPointer); $this->checkReferences( $phpcsFile, $tokenPointer, $references, $token['code'] === T_NEW ? $this->forbiddenClasses : $this->forbiddenExtends, ); } elseif ($token['code'] === T_DOUBLE_COLON && !$this->isTraitsConflictResolutionToken($token)) { $startTokenPointer = TokenHelper::findPreviousExcluding($phpcsFile, $nameTokens, $tokenPointer - 1); $references = $this->getAllReferences($phpcsFile, $startTokenPointer, $tokenPointer); $this->checkReferences($phpcsFile, $tokenPointer, $references, $this->forbiddenClasses); } } /** * @param list $references * @param array $forbiddenNames */ private function checkReferences( File $phpcsFile, int $tokenPointer, array $references, array $forbiddenNames, bool $isFixable = true ): void { $token = $phpcsFile->getTokens()[$tokenPointer]; $details = [ T_NEW => ['class', self::CODE_FORBIDDEN_CLASS], T_DOUBLE_COLON => ['class', self::CODE_FORBIDDEN_CLASS], T_EXTENDS => ['as a parent class', self::CODE_FORBIDDEN_PARENT_CLASS], T_IMPLEMENTS => ['interface', self::CODE_FORBIDDEN_INTERFACE], T_USE => ['trait', self::CODE_FORBIDDEN_TRAIT], ]; foreach ($references as $reference) { if (!array_key_exists($reference['fullyQualifiedName'], $forbiddenNames)) { continue; } $alternative = $forbiddenNames[$reference['fullyQualifiedName']]; [$nameType, $code] = $details[$token['code']]; if ($alternative === null) { $phpcsFile->addError( sprintf('Usage of %s %s is forbidden.', $reference['fullyQualifiedName'], $nameType), $reference['startPointer'], $code, ); } elseif (!$isFixable) { $phpcsFile->addError( sprintf( 'Usage of %s %s is forbidden, use %s instead.', $reference['fullyQualifiedName'], $nameType, $alternative, ), $reference['startPointer'], $code, ); } else { $fix = $phpcsFile->addFixableError( sprintf( 'Usage of %s %s is forbidden, use %s instead.', $reference['fullyQualifiedName'], $nameType, $alternative, ), $reference['startPointer'], $code, ); if (!$fix) { continue; } $phpcsFile->fixer->beginChangeset(); FixerHelper::change($phpcsFile, $reference['startPointer'], $reference['endPointer'], $alternative); $phpcsFile->fixer->endChangeset(); } } } /** * @param array|int|string> $token */ private function isTraitsConflictResolutionToken(array $token): bool { return is_array($token['conditions']) && array_pop($token['conditions']) === T_USE; } /** * @return list */ private function getAllReferences(File $phpcsFile, int $startPointer, int $endPointer): array { // Always ignore first token $startPointer++; $references = []; while ($startPointer < $endPointer) { $nextComma = TokenHelper::findNext($phpcsFile, [T_COMMA], $startPointer + 1); $nextSeparator = min($endPointer, $nextComma ?? PHP_INT_MAX); $reference = ReferencedNameHelper::getReferenceName($phpcsFile, $startPointer, $nextSeparator - 1); if ( strlen($reference) !== 0 && !in_array(strtolower($reference), self::$keywordReferences, true) ) { $references[] = [ 'fullyQualifiedName' => NamespaceHelper::resolveClassName($phpcsFile, $reference, $startPointer), 'startPointer' => TokenHelper::findNextEffective($phpcsFile, $startPointer, $endPointer), 'endPointer' => TokenHelper::findPreviousEffective($phpcsFile, $nextSeparator - 1, $startPointer), ]; } $startPointer = $nextSeparator + 1; } return $references; } /** * @param array $option * @return array */ private static function normalizeInputOption(array $option): array { $forbiddenClasses = []; foreach ($option as $forbiddenClass => $alternative) { $forbiddenClasses[self::normalizeClassName($forbiddenClass)] = self::normalizeClassName($alternative); } return $forbiddenClasses; } private static function normalizeClassName(?string $typeName): ?string { if ($typeName === null || strlen($typeName) === 0 || strtolower($typeName) === 'null') { return null; } return NamespaceHelper::getFullyQualifiedTypeName($typeName); } }